本系列文已改編成書「甚麼?網頁也可以做派對遊戲?使用 Vue 和 babylon.js 打造 3D 派對遊戲吧!」
書中不只重構了程式架構、改善了介面設計,還新增了 2 個新遊戲呦!ˋ( ° ▽、° )
新遊戲分別使用了陀螺儀與震動回饋,趕快買書來研究研究吧!ლ(╹∀╹ლ)
在此感謝深智數位的協助,歡迎大家前往購書,鱈魚感謝大家 (。・∀・)。
助教:「所以到底差在哪啊?沒圖沒真相,被你坑了都不知道。(´。_。`)」
鱈魚:「你對我是不是有甚麼很深的偏見啊 (っ °Д °;)っ,來人啊,上連結!」
再來先來實現「建立派對」的功能,讓我們準備開趴!
第一步先來建立頁面組件,並在 router 新增 RouteName 定義。
src\views\game-console.vue
<template>
<div class="flex">
我是 game-console
</div>
</template>
<script setup lang="ts">
import { ref } from 'vue';
</script>
<style scoped lang="sass">
</style>
src\router\router.ts
import { createRouter, createWebHistory, RouteRecordRaw } from 'vue-router'
export enum RouteName {
HOME = 'home',
GAME_CONSOLE = 'game-console',
}
const routes: Array<RouteRecordRaw> = [
{
path: '/',
redirect: {
name: 'home',
}
},
{
path: `/home`,
name: RouteName.HOME,
component: () => import('../views/the-home.vue')
},
{
path: `/game-console`,
name: RouteName.GAME_CONSOLE,
component: () => import('../views/game-console.vue'),
},
{
path: '/:pathMatch(.*)*',
redirect: '/'
},
]
const router = createRouter({
history: createWebHistory(),
routes
})
export default router
頁面建立完成後,讓我們在「建立派對」按鈕跳轉 route,跳來這一個頁面吧 ….?等等,這樣轉場不給力啊,遊戲應該要有遊戲專用的轉場才對不是嗎?
所以讓我們來實現自定義的 loading 效果吧。ヽ(✿゚▽゚)ノ
首先新增 loading.store,用來管理 loading 相關狀態。
src\stores\loading.store.ts
import { defineStore } from 'pinia';
interface State {
isLoading: boolean,
isEntering: boolean,
isLeaving: boolean,
/** loading 樣式,預留未來可以切換多種樣式 */
type: string,
}
export const useLoadingStore = defineStore('loading', {
state: (): State => ({
isLoading: false,
isEntering: false,
isLeaving: false,
type: ''
}),
})
接著新增一個包裝 Vue transition 組件的 transition-mask 組件,用來偵測過場狀態並提供轉場效果。
首先定義 Props。
src\components\transition-mask.vue
<script lang="ts">
export enum AnimationType {
ROUND = 'round',
}
</script>
<script setup lang="ts">
import { computed, reactive, ref, watch } from 'vue';
interface Props {
modelValue: boolean;
type?: `${AnimationType}`;
}
const props = withDefaults(defineProps<Props>(), {
type: AnimationType.ROUND,
});
...
</script>
如果 transition 組件可以設定 name 參數,用來指定欲使用的過場 class,這裡我們也提供 type 參數,作為相同概念用途。
接著新增狀態變數與各類轉場 hook,並將資料與事件綁定至 template 中。
<template>
<transition
:name="props.type"
@before-enter="handleBeforeEnter"
@after-enter="handleAfterEnter"
@before-leave="handleBeforeLeave"
@after-leave="handleAfterLeave"
>
<div
v-if="props.modelValue"
class="mask"
>
<slot />
</div>
</transition>
</template>
<script lang="ts">
...
export interface State {
isEntering: boolean,
isLeaving: boolean,
}
</script>
<script setup lang="ts">
...
const emit = defineEmits<{
(e: 'update', state: State): void;
}>();
const state = reactive<State>({
isEntering: false,
isLeaving: false,
});
watch(state, () => emit('update', state));
function handleBeforeEnter() {
state.isEntering = true;
state.isLeaving = false;
}
function handleAfterEnter() {
state.isEntering = false;
}
function handleBeforeLeave() {
state.isEntering = false;
state.isLeaving = true;
}
function handleAfterLeave() {
state.isLeaving = false;
}
defineExpose({
state
});
</script>
...
還差設計轉場用的 CSS,但是先讓我們把總體結構都完成,等等測試時再設計動畫。
新增 loading-overlay 組件,用來包裝所有的 loading 相關組件。
<template>
<transition-mask
class="absolute inset-0"
>
<div class="absolute inset-0 bg-white" />
</transition-mask>
</template>
<script setup lang="ts">
import { } from 'vue';
import TransitionMask from './transition-mask.vue';
</script>
接著新增 use-loading,用來提供所有 loading 功能。
src\composables\use-loading.ts
import { promiseTimeout, until, watchOnce } from '@vueuse/core';
import { defaults } from 'lodash-es';
import { storeToRefs } from 'pinia';
import { ref, watch } from 'vue'
import { useLoadingStore } from '../stores/loading.store'
export interface State {
isEntering: boolean,
isLeaving: boolean,
}
interface UseLoadingParams {
/** 最小持續時間 (ms,預設 1000)
*
* 讀取頁面進入完成至開始離開之間的最小時間,可用來展示動畫。
*/
minDuration?: number;
}
const DefaultParams: Required<UseLoadingParams> = {
minDuration: 1000,
}
export function useLoading(paramsIn?: UseLoadingParams) {
const params = defaults(paramsIn, DefaultParams);
const store = useLoadingStore();
const { isLoading, isEntering, isLeaving } = storeToRefs(store);
const visible = ref(false);
const minDuration = params.minDuration;
watch(isLoading, (value) => {
visible.value = value;
}, {
immediate: true
});
watch(visible, async (value) => {
// show() 要立即顯示
if (value) {
store.$patch({
isLoading: true,
});
return;
}
await promiseTimeout(minDuration);
// hide() entering 為 false,直接隱藏
if (!isEntering.value) {
store.$patch({
isLoading: false,
});
return;
}
// entering 結束之後才可隱藏
watchOnce(isEntering, () => {
store.$patch({
isLoading: false,
});
})
}, {
deep: true
});
/**
* 等到過場動畫進入完成後,才會完成 Promise
* 可以避免動畫還沒完成就跳頁的問題
*/
async function show() {
visible.value = true;
await until(isEntering).toBe(true);
await until(isEntering).toBe(false);
}
/** 等到過場動畫離開完成後,才會完成 Promise */
async function hide() {
visible.value = false;
await until(isLeaving).toBe(true);
await until(isLeaving).toBe(false);
}
function handleUpdate({ isEntering, isLeaving }: State) {
store.$patch({
isEntering,
isLeaving
});
}
return {
isLoading,
isEntering,
isLeaving,
show,
hide,
handleUpdate
}
}
在 loading-overlay 中導入 use-loading,綁定狀態吧!
<template>
<transition-mask
v-model="isLoading"
class="absolute inset-0"
@update="handleUpdate"
>
<div class="absolute inset-0 bg-white" />
</transition-mask>
</template>
<script setup lang="ts">
import { } from 'vue';
import { useLoading } from '../composables/use-loading';
import TransitionMask from './transition-mask.vue';
const { isLoading, handleUpdate } = useLoading();
</script>
現在讓我們把 loading-overlay 加到 App.vue 並使用 use-loading,實測看看過場效果。
src\App.vue
<template>
<router-view />
<loading-overlay />
</template>
<script setup lang="ts">
import { ref } from 'vue';
import LoadingOverlay from './components/loading-overlay.vue';
import { useLoading } from './composables/use-loading';
const loading = useLoading();
setTimeout(() => {
loading.show();
setTimeout(() => {
loading.hide();
}, 2000);
}, 2000);
document.title += ` v${import.meta.env.PACKAGE_VERSION}`;
</script>
大家一定會覺得很奇怪,怎麼會是 2 秒後忽然全白,再 2 秒後白色消失,就這樣?(⊙_⊙)
別擔心,那是因為我們還沒在 transition-mask 中加入轉場用 class,所以只會瞬間進入、瞬間消失。
現在讓我們加上轉場用 class。
src\components\transition-mask.vue
...
<script setup lang="ts">
...
</script>
<style scoped lang="sass">
.round-enter-active, .round-leave-active
transition-duration: 0.4s
.round-enter-from, .round-leave-to
opacity: 0 !important
</style>
現在是不是變成 2 秒後漸入全白,再 2 秒後全白漸出呢?這就表示 loading 功能正常運作了!
現在讓我們在 transition-mask 加入預期的轉場 class 吧!♪(´▽`)
src\components\transition-mask.vue
...
<script setup lang="ts">
...
</script>
<style scoped lang="sass">
.round-enter-active
animation-duration: 1.4s
.round-leave-active
transition-duration: 0.4s
transition-timing-function: ease-in-out
.round-enter-from, .round-enter-to
animation-name: round-in
animation-fill-mode: forwards
@keyframes round-in
0%
clip-path: circle(3% at 46% -50%)
animation-timing-function: cubic-bezier(0.005, 0.920, 0.060, 0.99)
40%
clip-path: circle(3% at 50% 50%)
animation-timing-function: cubic-bezier(0.630, -0.170, 0.140, 0.980)
100%
clip-path: circle(70.7% at 50% 50%)
.round-leave-from
clip-path: circle(70.7% at 50% 50%)
.round-leave-to
clip-path: circle(40% at 140% 140%)
</style>
目前過場動畫應該會如下圖。
大成功!ヽ(✿゚▽゚)ノ
助教:「只有白色的畫面,還敢端出來啊?(´。_。`)」
鱈魚:「當然不是只有這樣 ( •̀ ω •́ )✧,接下來讓我們加入 QQ 的載入畫面吧!」
以上程式碼已同步至 GitLab,大家可以前往下載: